/*
 * Copyright (c) 2009-2012 Panxiaobo
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package p.cod;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.googlecode.dex2jar.reader.io.ArrayDataIn;
import com.googlecode.dex2jar.reader.io.DataIn;

public class CodFileReader {
    public static final int Library = 0x1;
    public static final int Midlet = 0x2;
    public static final int Parseable = 0x4;
    public static final int Brittle = 0x10;
    public static final int Platform = 0x20;
    public static final int NoVerifyErr = 0x40;

    public static final int ATTR_CLZ_Inner = 0x200;
    public static final int ATTR_CLZ_UnGroupable = 0x100;
    public static final int ATTR_CLZ_Persistable = 0x80;
    public static final int ATTR_CLZ_HasVerifyError = 0x40;
    public static final int ATTR_CLZ_Interface = 0x20;
    public static final int ATTR_CLZ_Abstract = 0x10;
    public static final int ATTR_CLZ_Public = 0x1;
    public static final int ATTR_CLZ_Private = 0x2;
    public static final int ATTR_CLZ_Protected = 0x4;
    public static final int ATTR_CLZ_Final = 0x8;

    public static String[] CompressedString = { "", "in", "et", "it", "init", "init>", "de", "ce", "get", "cl",
            "<init>", "er", "re", "<cl", "<clinit>", "im", "on", "at", "vi", "en", "vice", "rim", "net", "device",
            "ap", "or", "api", "st", "ion", "pt", "set", "al", "ro", "an", "ec", "ed", "$", "ad", "St", "th", "In",
            "ss", "ert", "Pro", "am", "ry", ".", "ord", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "ata", "em",
            "<", "rypt", ">", "ut", "ar", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
            "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "co", "pert", "ic", "crypt", "_", "us", "a", "b",
            "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w",
            "x", "y", "z", "Propert", "Property", "ey", "le", "Data", "va", "se", "ate", "ava", "ing", "Rec", "Val",
            "java", "ption", "oc", "ent", "el", "ang", "io", "id", "um", "rit", "crypto", "yst", "ystem", "Ex",
            "Record", "ch", "Exce", "Exception", "read", "is", "gth", "ort", "ength", "ist", "int", "Re", "Key", "un",
            "mp", "writ", "write", "Co", "la", "By", "Length", "ui", "gr", "ress", "ac", "ur", "gram", "to", "ig",
            "Fi", "add", "ex", "dex", "Datagram", "PropertyVal", "Ch", "iv", "Index", "ring", "ont", "od", "eld",
            "Field", "String", "ase", "ation", "ect", "ll", "Of", "ocus", "ag", "List", "end", "Ad", "cld", "cldc",
            "lic", "ra", "up", "comp", "rec", "ran", "record", "Focus", "ow", "rans", "ext", "te", "ew", "getP", "il",
            "ener", "umb", "op", "iz", "getM", "lang", "system", "System", "base", "age", "der", "ip", "No", "He",
            "key", "Listener", "ize", "ub", "thumb", "Up", "Stre", "Id", "pa", "Stream", "open", "ess", "Stat", "out",
            "ange", "send", "port", "idth", "essage", "ition", "ime", "\u00FF" };

    public CodFileReader(byte[] bytes) throws IOException {

        DataIn in = ArrayDataIn.le(bytes);
        int flashid = in.readIntx();
        if (0xffffc0de != flashid) {
            throw new IOException("not a cod format");
        }
        // System.out.printf("%08x flashid\n", flashid);
        in.skip(5 * 4);
        int max_type_list_size = in.readUShortx();
        // System.out.printf("%04x max_type_list_size=\n", max_type_list_size);
        in.skip(2 + 2 * 4);
        version = in.readUShortx();
        int codesize = in.readUShortx();
        int datasize = in.readUShortx();
        // System.out.printf("%04x version=\n", version);
        // System.out.printf("%04x codesize=\n", codesize);
        // System.out.printf("%04x datasize=\n", datasize);

        int flags = in.readUShortx();
        // System.out.printf("%04x flags=\n", flags);

        int codeStart = in.getCurrentPosition();
        // System.out.printf("%08x codeStart\n", codeStart);

        codeIn = ArrayDataIn.le(bytes, codeStart, codesize);
        dataIn = ArrayDataIn.le(bytes, codeStart + codesize, datasize);
    }

    int version;
    DataIn codeIn;
    DataIn dataIn;
    Module thisModule;
    List<Clz> classReferences;
    Map<Integer, Object> fixItems;

    public void data() {
        new ReadForFix(this).fix();
        data2();
    }

    private void data2() {
        DataIn in = dataIn;
        in.move(0);
        in.skip(5);
        int classes = in.readUByte();
        in.skip(16 * 2);
        in.skip(14);

        for (int i = 0; i < classes; i++) {
            int clz_offset = in.readUShortx();
            readClass(clz_offset);
        }

        // in.move(absOffset)
    }

    public int readLeb(DataIn in) {
        int i = 0;
        int j = 0;
        while (true) {
            int k = in.readUByte();
            i = ((k & 0x7F) << j) + i;
            j += 7;
            if ((k & 0x80) == 0)
                break;
        }
        return i & 0xFFFF;
    }

    public final int skipSlack(DataIn in, int paramInt) {
        int i = paramInt - 1;
        int offset = in.getCurrentPosition();
        int j = (offset + i & (i ^ 0xFFFFFFFF)) - offset;
        for (int k = 0; k < j; k++) {
            if (in.readByte() == 0)
                continue;
            throw new RuntimeException("bad slack byte at offset: 0x" + Integer.toHexString(offset));
        }
        return j;
    }

    String getIdentifier(int a) {
        return getString(a);
    }

    String getLiteralString(int offset) {
        if ((offset == 0) || (offset == 65535))
            throw new RuntimeException();
        DataIn in = dataIn;
        in.pushMove(offset);
        try {
            StringBuilder sb = new StringBuilder(50);
            for (int m = in.readUByte(); m != 0; m = in.readUByte()) {
                sb.append((char) m);
            }
            return sb.toString();
        } finally {
            in.pop();
        }
    }

    String dumpClzAttr(int i) {
        StringBuilder sb = new StringBuilder();
        if ((i & 0x200) != 0)
            sb.append("IsInner ");
        if ((i & 0x100) != 0)
            sb.append("IsUnGroupable ");
        if ((i & 0x80) != 0)
            sb.append("IsPersistable ");
        if ((i & 0x40) != 0)
            sb.append("HasVerifyError ");
        if ((i & 0x20) != 0)
            sb.append("IsInterface ");
        if ((i & 0x10) != 0)
            sb.append("IsAbstract ");
        if ((i & 0x1) != 0)
            sb.append("IsPublic ");
        if ((i & 0x2) != 0)
            sb.append("IsPrivate ");
        if ((i & 0x4) != 0)
            sb.append("IsProtected ");
        if ((i & 0x8) != 0)
            sb.append("IsFinal ");
        return sb.toString();
    }

    void readClass(int offset) {
        DataIn in = dataIn;
        in.pushMove(offset);
        int pkg_offset = in.readUShortx();
        int name_offset = in.readUShortx();
        // Clz clz = classMap.get((pkg_offset << 16) | name_offset);

        int a = in.readUByte();
        int b = in.readUByte();

        int staticStart = in.readUShortx();
        int _cinit = in.readUShortx();
        int _init = in.readUShortx();

        // System.out.println(String.format("%04d %04d %04d", staticStart, _cinit, _init));

        in.skip(6);// createsize, secureindex, index

        int code_start = in.readUShortx();
        int code_end = in.readUShortx();

        int attributes = in.readUShortx();
        System.out.println(String.format("%04d %s", attributes, this.dumpClzAttr(attributes)));

        int virtualRoutinesOffset = (in.readUShortx() + offset);
        int nonVirtualRoutinesOffset = (in.readUShortx() + offset);
        int staticRoutinesOffset = (in.readUShortx() + offset);
        int fieldDefinitionsOffset = (in.readUShortx() + offset);
        int staticFieldDefinitionsOffset = (in.readUShortx() + offset);
        int implementedInterfacesOffset = (in.readUShortx() + offset);
        int fieldDefinitionAttributesOffset = (in.readUShortx() + offset);
        int staticFieldDefinitionAttributesOffset = (in.readUShortx() + offset);

        readRoutines(in, virtualRoutinesOffset, nonVirtualRoutinesOffset, "virtual");
        readRoutines(in, nonVirtualRoutinesOffset, staticRoutinesOffset, "non-virtual");
        readRoutines(in, staticRoutinesOffset, fieldDefinitionsOffset, "static");

        // int fieldCount = readFields(in, fieldDefinitionsOffset, staticFieldDefinitionsOffset, false, clz);
        // int staticFieldCount = readFields(in, fieldDefinitionsOffset, implementedInterfacesOffset, true, clz);

        readInterfaces(in, implementedInterfacesOffset, fieldDefinitionAttributesOffset);

        // readFieldAttrs(in, fieldDefinitionAttributesOffset, fieldCount);
        // readFieldAttrs(in, staticFieldDefinitionAttributesOffset, staticFieldCount);

        in.pop();
    }

    private void readFieldAttrs(DataIn in, int a, int fieldCount) {
        if (a != in.getCurrentPosition()) {
            System.out.println("");
        }
        for (int i = 0; i < fieldCount; i++) {
            int attr = in.readUByte();
        }
    }

    void readRoutines(DataIn in, int a, int b, String name) {
        int s = (b - a) / 2;
        if (s > 0) {
            for (int i = 0; i < s; i++) {
                int routineOffset = in.readUShortx();
                System.out.println(String.format("%04d %s-routines", routineOffset, name));
                readRoutine(routineOffset);
            }
        }
    }

    private void readRoutine(int offset) {
        DataIn in = this.codeIn;
        in.pushMove(offset);
        in.push();
        in.move(offset - 5);
        boolean xxxx = in.readUByte() > 1;
        in.pop();
        int headOffset = xxxx ? 9 : 14;
        in.move(offset - headOffset);
        int codeSize, nameOffset, prototypeOffset, xreturnOffset;
        if (xxxx) {
            xreturnOffset = in.readUShortx();
            prototypeOffset = in.readUShortx();
            codeSize = in.readUByte() - 2;
            int attr = in.readUByte();
            nameOffset = in.readUShortx();
            int nlps = in.readUByte();
            int numstackmaps = nlps >> 6 & 0x3;
            int locals = nlps >> 4 & 0x3;
            int parms = nlps >> 2 & 0x3;
            int stack = nlps >> 0 & 0x3;
        } else {
            nameOffset = in.readUShortx();
            prototypeOffset = in.readUShortx();
            xreturnOffset = in.readUShortx();
            codeSize = in.readUShortx();

            int attr = in.readUShortx();

            int numstackmaps = in.readUByte();
            int locals = in.readUByte();
            int parms = in.readUByte();
            int stack = in.readUByte();
        }

        String name = getIdentifier(nameOffset);
        List<String> ret = doTypeList(xreturnOffset);
        List<String> prototype = doTypeList(prototypeOffset);
        System.out.println(String.format("%s, %s, %s", name, ret, prototype));
        new CodeReader(this).read(in, codeSize);
        in.pop();

    }

    public static final byte[] OP_SIZE = { 1, 4, 6, 4, 5, 4, 5, 4, 5, 4, 4, 4, 3, 4, 1, 4, 1, 4, 1, 2, 1, 2, 3, 5, 1,
            2, 2, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2, 3, 5, 9, 3, 1, 5, 1, 1, 6, 1, 0, 1, 2, 3, 2, 3, 2, 3, 1, 1, 1, 1, 1, 1,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2, 3, 2, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2,
            2, 2, 2, 2, 2, 2, 2, 2, 4, 5, 4, 5, 4, 5, 4, 5, 1, 1, 1, 1, 1, 1, 1, 3, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 0, 0, 2, 4, 1, 2,
            3, 4, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2, 3, 1, 3, 3, 2, 3, 2, 3, 4, 5, 5, 3, 4, 3, 4, 1, 1, 1,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 5, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
            1, 1, 1, 1, 1, 0, 3, 3, 1, 1, 1, 1, 1, 2, 3, -1 };

    int readFields(DataIn in, int a, int b, boolean isStatic, Clz clz) {
        int size = isStatic ? 6 : 4;
        int s = (b - a) / size;
        if (s > 0) {
            for (int i = 0; i < s; i++) {
                int nameOffset = in.readUShortx();
                int types = in.readUShortx();
                Field f = new Field(nameOffset, types);
                f.index = i;
                if (isStatic) {
                    int x = in.readUShortx();
                    clz.staticFieldMap.put(x, f);
                } else {
                    clz.fields.add(f);
                }
            }
        }
        return s;
    }

    private List<String> doTypeList(int offset) {
        // System.err.println(String.format("TYPE_LIST %08x", offset));
        if (offset == 0) {
            return Collections.EMPTY_LIST;
        }
        List<String> list = new ArrayList();
        DataIn in = this.dataIn;
        in.pushMove(offset);
        try {
            int i = in.readUByte();
            int j = 0;
            if ((i & 0x80) == 0) {
                j = (i & 0x70) >> 4;
            } else {
                j = i & 0x7F;
                i = in.readUByte();
                if ((j & 0x40) != 0) {
                    j &= -65;
                    j <<= 4;
                    j += ((i & 0xF0) >> 4);
                }
                j--;
            }
            if (j > 0) {
                int end = in.getCurrentPosition() + j - 1;
                list.add(xtype(in, i));
                while (in.getCurrentPosition() < end) {
                    i = in.readUByte();
                    String s = xtype(in, i);
                    i >>= 4;
                    if (i != 0) {
                    }
                    do {
                        list.add(s);
                    } while (i-- > 0);
                }
            }
        } finally {
            in.pop();
        }
        return list;
    }

    private static String[] TYPE_NAMES = { null, "boolean", "byte", "char", "short", "int", "long", null, null, null,
            "void", "float", "double" };

    private String xtype(DataIn in, int type) {
        type &= 15;
        int a, b;
        switch (type) {
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
        case 6:
        case 10:
        case 11:
        case 12:
            return TYPE_NAMES[type];

        case 8:
            int arrayDemo = in.readUByte();
            int nestType = in.readUByte();
            String s;
            if (nestType == 7) {
                a = in.readUByte();
                b = in.readUByte();
                s = getClassDefB(a, b).toString();
            } else {
                s = TYPE_NAMES[nestType];
            }
            for (int i = 0; i < arrayDemo; i++) {
                s += "[]";
            }
            return s;
        case 7:
        case 9:
            a = in.readUByte();
            b = in.readUByte();
            return getClassDefB(a, b).toString();
        default:
            throw new RuntimeException("unexpected type id: 0x" + Integer.toHexString(type) + " at offset: 0x"
                    + Integer.toHexString(type));
        }
    }

    public static class Module {
        int index;
        String name;
        String version;
        List<Clz> classes = new ArrayList();
        List<Routine> rountines = new ArrayList();

        public void addClz(Clz clz) {
            clz.index = classes.size();
            clz.module = this;
            classes.add(clz);
        }

        public String toString() {
            return name + " - " + version;
        }
    }

    public class Clz {
        public Clz(int pkg, int clz) {
            super();
            this.pkg = pkg;
            this.clz = clz;
        }

        public String toString() {
            return getString(pkg) + "." + getString(clz);
        }

        Module module;
        int index, pkg, clz;

        List<Field> fields = new ArrayList();
        List<Field> staticFields = new ArrayList();
        Map<Integer, Field> staticFieldMap = new HashMap();
        List<Routine> virtualRoutines = new ArrayList();
        List<Routine> staticRoutines = new ArrayList();
        List<Routine> nonVirtualRoutines = new ArrayList();
    }

    class Field {
        int name, typeList, index, offset, staticPosition;
        Clz owner;

        public String toString() {
            return owner + "." + getString(name) + " " + doTypeList(typeList);
        }

        public Field(int name, int typeList) {
            super();
            this.name = name;
            this.typeList = typeList;
        }
    }

    class Routine {
        Clz owner;
        public int index, offset;
        public int name;
        public int proto;
        public int xreturn;

        public String toString() {
            return owner + "." + getString(name) + " " + doTypeList(proto) + " " + doTypeList(xreturn);
        }
    }

    Clz getClassDefB(int moduleIdx, int clzIdx) {
        if (moduleIdx == 255) {
            return null;
        }
        if (moduleIdx == 0) {
            return thisModule.classes.get(clzIdx);
        }
        return classReferences.get(clzIdx);
    }

    void readInterfaces(DataIn in, int a, int b) {
        if (a != in.getCurrentPosition()) {
            System.out.println("");
        }
        int s = (b - a) / 2;
        if (s > 0) {
            for (int i = 0; i < s; i++) {
                int module = in.readUByte();
                int clz = in.readUByte();
            }
        }
    }

    String getString(int offset) {
        DataIn in = dataIn;
        in.pushMove(offset);
        try {
            StringBuilder sb = new StringBuilder(50);
            for (int m = in.readUByte(); m != 0; m = in.readUByte()) {
                if (m == 255) {
                    m = in.readUByte();
                    sb.append((char) m);
                } else {
                    sb.append(CompressedString[m]);
                }
            }
            return sb.toString();
        } finally {
            in.pop();
        }
    }

}
